package main.Ontology;

import java.io.Serializable;
import java.util.*;

/**
 * @author Jared Hance
 * 
 *         A set of ElementOntology's which creates the overall ontology for an
 *         element, containing all of the ontologies of that element.
 */
public class ElementOntologySet implements Comparable<ElementOntologySet>, Serializable, Cloneable {
    private final Map<Class<? extends ElementOntology>, ElementOntology> base;
    private final Map<Object, ElementOntology> cache;

    /**
     * Creates a new ElementOntologySet with no elements using the given cache
     * 
     * @param cache
     *            A cache, which should be shared between all related sets
     */
    public ElementOntologySet(Map<Object, ElementOntology> cache) {
        this.base = new LinkedHashMap<Class<? extends ElementOntology>, ElementOntology>();
        this.cache = cache;
    }

    /**
     * Creates a new ElementOntologySet with the same elements as the other set
     * and sharing the same cache
     * 
     * @param other
     *            Another set to be copied
     */
    public ElementOntologySet(ElementOntologySet other) {
        this.base = other.base;
        this.cache = other.cache;
    }

    /**
     * Creates a new ElementOntologySet with a given base and cache; used
     * internally because base cannot be set outside of the constructor
     * 
     * @param base
     *            A base map used to store the data
     * @param cache
     *            A cache, which should be shared between all related sets
     */
    protected ElementOntologySet(
            Map<Class<? extends ElementOntology>, ElementOntology> base,
            Map<Object, ElementOntology> cache) {
        this.base = base;
        this.cache = cache;
    }

    /**
     * Useful to create a new ElementOntologySet that shares the same cache as
     * an old one
     * 
     * @return The cache used by this set
     */
    public Map<Object, ElementOntology> getCache() {
        return this.cache;
    }

    /**
     * Adds an ontology to the set if it isn't already in the set.
     * 
     * @param ontology
     * @return A cached ontology; the old ontology should be replaced by the returned one
     */
    public ElementOntology add(ElementOntology ontology) {
        if(ontology==null || ontology.getName()==null)
            return ontology;
        ontology = getCachedOntology(ontology);
        base.put(ontology.getClass(), ontology);
        return ontology;
    }

    /**
     * Gets the ElementOntology associated with a particular class, such as
     * DocumentTypeOntology.
     * 
     * @param key
     *            A class, like DocumentTypeOntology.class
     * @return The ElementOntology associated with that class
     */
    public ElementOntology get(Class<? extends ElementOntology> key) {
        return base.get(key);
    }

    /**
     * Gets the first item in the ordered set
     * 
     * @return The first item pair
     */
    public ElementOntology getHead() {
        Set<Class<? extends ElementOntology>> keys = base.keySet();
        Iterator<Class<? extends ElementOntology>> keysIter = keys.iterator();
        
        if(!keysIter.hasNext()) return null;
        
        Class<? extends ElementOntology> key = keysIter.next();
        return base.get(key);
    }

    /**
     * Gets all of the items in the set excluding the head (first item)
     * 
     * @return A new set not containing the element given by getHead()
     */
    public ElementOntologySet getTail() {
        Iterator<Class<? extends ElementOntology>> keysIter = base.keySet()
                .iterator();
        Iterator<ElementOntology> valuesIter = base.values().iterator();
        if(!keysIter.hasNext() || !valuesIter.hasNext())
            return null;
        
        keysIter.next();
        valuesIter.next();
        
        Map<Class<? extends ElementOntology>, ElementOntology> newBase = new LinkedHashMap<Class<? extends ElementOntology>, ElementOntology>();
        while (keysIter.hasNext() && valuesIter.hasNext()) {
            newBase.put(keysIter.next(), valuesIter.next());
        }
        return new ElementOntologySet(newBase, cache);
    }

    /**
     * Gets an iterator over the values of the set; the "key" used in get can be
     * obtained by using getClass() on the value.
     * 
     * @return An iterator over the values in the set
     */
    public Iterator<ElementOntology> getIterator() {
        return base.values().iterator();
    }
    
    public boolean isEmpty() {
        return base.isEmpty();
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof ElementOntologySet) {
            ElementOntologySet other = (ElementOntologySet) obj;
            return this.base.equals(other.base);
        }
        return false;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 59 * hash + (this.base != null ? this.base.hashCode() : 0);
        return hash;
    }

    

    @Override
    public String toString() {
        Iterator<ElementOntology> iter = getIterator();
        if(!iter.hasNext())
            return "";
//        StringBuilder builder = new StringBuilder(iter.next().getName());
        StringBuilder builder = new StringBuilder(iter.next().getLabel());
        
        while(iter.hasNext()) {
            builder.append(".");
            builder.append(iter.next());
        }
        
        return builder.toString();
    }
    /**
     * Both ElementOntologySet's should have the same cache (created with the
     * same factory)
     * 
     * @param s1
     * @param s2
     * @return The intersection of the two sets
     */
    public static ElementOntologySet intersection(ElementOntologySet s1,
            ElementOntologySet s2) {
        if (s1.cache != s2.cache) {
            throw new IllegalArgumentException(
                    "Both ElementOntologySet's should have the same cache");
        }

        Iterator<Class<? extends ElementOntology>> keyIter = s1.base.keySet()
                .iterator();
        Iterator<ElementOntology> valueIter = s1.base.values().iterator();

        Map<Class<? extends ElementOntology>, ElementOntology> newBase = new LinkedHashMap<Class<? extends ElementOntology>, ElementOntology>();
        while (keyIter.hasNext() && valueIter.hasNext()) {
            Class<? extends ElementOntology> key = keyIter.next();
            ElementOntology value = valueIter.next();

            if (s2.base.containsKey(key) && s2.base.containsValue(value)) {
                newBase.put(key, value);
            }
        }

        return new ElementOntologySet(newBase, s1.cache);
    }
    
    public ElementOntology getCachedOntology(ElementOntology ontology) {
        Object wrapper = new ElementOntologyWrapper(ontology);
        ElementOntology cachedOntology = cache.get(wrapper);
        if (cachedOntology == null) {
            cache.put(wrapper, ontology);
            return ontology;
        } else {
            return cachedOntology;
        }
    }

    @Override
    public int compareTo(ElementOntologySet o) {
        return equals(o) ? 0:-1;
    }

    /**
     * @author Jared Hance
     * 
     *         Used to override equals() to do value comparison for the keys of
     *         the base set. It is, essentially, an Object. It's
     *         ElementOntologyWrapper type doesn't need to be used, so the
     *         actual type of the object doesn't need to leak out into
     *         ElementOntologyFactory's implementation.
     */
    private static class ElementOntologyWrapper {
        private ElementOntology base;

        public ElementOntologyWrapper(ElementOntology base) {
            this.base = base;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof ElementOntologyWrapper) {
                ElementOntologyWrapper other = (ElementOntologyWrapper) obj;
                return this.base.getClass().equals(other.base.getClass())
                        && this.base.getName().equals(other.base.getName());
            }
            return false;
        }

        @Override
        public int hashCode() {
            String name = base.getName();
            Class<? extends ElementOntology> klass = base.getClass();

            return name.hashCode() + klass.hashCode();
        }
    }
}
